Godaddy服务器上关于ASP.NET网站建设一些经验 - 断点续传下载 (二)
发布时间:2013-11-22 浏览:339打印字号:大中小
谈谈在APS.NET中如何控制文件下载.
设计目的和要求
假设这么一个应用场景:
一个主机,上面存有许多文件资料,有各种文件格式.(PDF, DOC, EXE ... 等等).
该主机上运行一个ASP.NET网站, 用户注册,并付费之后允许他/她下载资料.
文件是放在IIS服务器上的, 如果用户知道具体路径那么他是可以随时下载的. (在没有或者不能设置访问权限的情况下.)
如果直接把下载路径发送给付费用户,肯定是行不通的,会被散播出去. 所以不能把让客户端得知具体路径,文件内容由 ASP.NET 服务器页面读取后发送给客户端.
要做的就是: 编写一个ASP.NET 页面服务器代码, 读取指定文件,并发送给客户 .
总体思路
.net 里, 有2个函数可以用来发送文件 Response.WriteFile 和 Response.TransmiteFile
它们的主要区别是: WriteFile 是先把文件内容读取到服务器缓冲,然后再发送到客户端. 所以对于大文件,会造成服务器很大的压力.
一般用来处理小文件,比如,发送给 excel 报表之类的. TransmiteFile 不缓冲数据, 直接抛给客户端, 所以可以用来发大文件.
( 采用 TransmiteFile 来实现.)
具体实现
1. 给客户一个链接,形如 http://xxxx/downloads.aspx?Key=ABCD123456
2. 在downloads.aspx的服务器代码中, 通过Key的值,查询数据库,得到服务器上的真实文件路径. 这个时候,控制权在 downloads.aspx, 所以可以编写复杂的控制功能, 比如看看用户有没有登录,有没有付费之类的,从而避免外部盗链.
3. 得到文件路径后,调用 Response.TransmiteFile 发送文件给客户端.
4. 因为给客户的链接里没有任何文件名的信息, 所以要在HTTP响应头里添加一句,告诉客户端文件名: Response.AddHeader("Content-Disposition", "attachment; filename=/"" + 你的文件名 + "/""); (如果要支持中文,要考虑编码的问题, 我这里不说,不是我们的主题.)
5. 如果是一个大文件, 比如1G, 不支持断点续传,是没有意义的. 那么如何实现呢?
(1) 要让客户端知道我们的服务器支持断点续传, 要在HTTP响应头中包含 Accept-Ranges: bytes 和 ETag: "XXXX".
ETag 是一个文件的标识, 供客户端判断它请求的是同一个文件, ETag 的内容在HTTP规范里并没有具体要求,只要保证在同一个服务器上,同一个文件有相同的ETag 就行了, 一般就根据文件名和最后修改时间生成一个字符串就可以了.
代码示例:
Response.AddHeader("Accept-Ranges", "bytes"); // 断点续传控制.
Response.AddHeader("ETag", "/"" + strETag + "/""); // 允许断点续传
(2) 要处理客户端请求中的 "Range" 字段. 一般格式是这样: Range: bytes=1234- 或者 Range: bytes=1234-12345
分别表示从地1235个字节开始下载和下载第1235到第12346个字节之间的数据.
服务器首先要添加 Content-Range 响应头, 然后用 TransmiteFile 发送指定的数据.
代码示例:
Response.StatusCode = 206;
Response.AddHeader("Content-Length", (lTo - lFrom + 1).ToString());
Response.AddHeader("Content-Range", string.Format("bytes {0}-{1}/{2}", lFrom, lTo, fi.Length)); // 参数0 和 参数1 是位置. 参数2是文件长度
Response.TransmitFile(strFilePath, lFrom, lTo - lFrom + 1);
( 其中, lFrom 和 lTo 是根据客户端请求中的 Range 字段得到的.)
说一下优缺点:
1. 可以随心所欲的控制下载.
2. 可以绕过服务器文件类型下载的限制, 比如服务器不允许下载 ISO 和 NRG 文件扩展名的文件, 如果直接输入RUL会提示404, 但是用上述的方法可以下载.
3.用这种办法的话,下载是在.net的一个线程里做的,如果用户量大的话,需要维护多个响应
附注:
1. TransmitFile(String) ( 函数是 .net 2.0 才加上去的.
2. TransmitFile(String, Int64, Int64) 带发送位置参数的重载是 .net 2.0 sp1 以后才支持的. 所以要用本文所说的方法实现断点续传, 至少要支持.net 2.0 sp1
3. 没有检测请求头中的 If-Range 和 Unless-Modified-Since, 如果有需要,在得到文件名之后就可以校验一下, 分别对应 ETag 和 Last-Modified.
// 1. 获取服务器上的文件路径 // 这里,如果文件路径有问题, 无法映射则会抛出异常, strURL 是根据 Key从数据库中查询到的真实文件路径
string strFilePath = Server.MapPath("~" + strURL);
// 2. 获取文件名
string strFileName = System.IO.Path.GetFileName(strFilePath);
// 3. 确认文件是否存在
FileInfo fi = new FileInfo(strFilePath);
if (!fi.Exists)
{
// 退出点,文件不存在
}
// 4. 抛给客户端
strFileName.Replace(" ", "%20"); // 处理文件名含空格的情况
string strETag = strFileName.ToUpper() + ":" + fi.Length.ToString(); // 我的Etag 是用文件名和字节数构成,马马虎虎凑合用.
string strLastTime = fi.LastWriteTimeUtc.ToString("r");
Response.Clear(); // 先把响应流清空
Response.ContentType = "application/octet-stream"; // 指定文件类型,使客户端总是弹出保存文件的框框.
Response.AddHeader("Content-Disposition", "attachment; filename=/"" + strFileName + "/"");
Response.AddHeader("Accept-Ranges", "bytes"); // 断点续传控制.
Response.AddHeader("ETag", "/"" + strETag + "/""); // 允许断点续传
Response.AddHeader("Last-Modified", strLastTime);//把最后修改日期写入响应
// 获取客户端请求的范围, 并且要校验这个范围的有效性
long lFrom = 0;
long lTo = 0;
bool bParts = false;
string strRange = Request.Headers["Range"];
if (ParseRange(strRange, out lFrom, out lTo)) /// ParseRange 是我自己写的函数, 从 Range 中读取2个位置.代码在后面.
{
if (-1 == lFrom && -1 == lTo)
{
// 不允许2个值都不指定
}
else
{
if (lTo == -1) lTo = fi.Length - 1; // 客户端未指定结束位置,则认为是文件的最后一个字符 Range: bytes=123- 的情况
if (lFrom == -1) // Range: bytes=-123 的情况, 请求最后的123个字节
{
lFrom = fi.Length - lTo;
lTo = fi.Length - 1;
}
if (lFrom < 0 || lFrom >= fi.Length || lFrom > lTo || lTo < 0 || lTo >= fi.Length)
{
// 以上几种情况下,范围的值能解析出来,但是不合法.
// 首先 From 和 To 的下标都应该在文件长度范围内
// 其次 From 应该 <= To
}
else
{
bParts = true;
}
}
}
// 根据用户请求,返回数据段或者整个文件
if(bParts)
{
Response.StatusCode = 206;
Response.AddHeader("Content-Length", (lTo - lFrom + 1).ToString());
Response.AddHeader("Content-Range", string.Format("bytes {0}-{1}/{2}", lFrom, lTo, fi.Length)); // 参数0 和 参数1 是位置,从0开始. 参数2是文件长度
Response.TransmitFile(strFilePath, lFrom, lTo - lFrom + 1);
}
else
{
Response.AddHeader("Content-Length", fi.Length.ToString());
Response.TransmitFile(strFilePath);
}
Response.End();
}
=============================传说中的分割线======================================
protected bool ParseRange(string strRange, out long lFrom, out long lTo)
{
lFrom = 0;
lTo = 0;
long lTemp = 0;
if (strRange == null || strRange == "")
{
return false; // 字符串为空
}
else
{
strRange = strRange.Replace(" ", ""); // 去除多余的空格
string[] range = strRange.Split(new char[] { '=', '-' });
// 1.分割后,包含3段 第一段是 "Range: bytes", 第二段是起始位置, 第三段是结束位置
if (range.Length != 3)
{
return false; // 格式不正确 只支持 Range: bytes=89294317- 或者 Range: bytes=1234-1235 或者 Range: bytes=-500 3种格式.
}
// 2. 解析起始位置
if (range[1].Length <= 0)
{
// 起始位置未指定
lFrom = -1;
}
else
{
if (!long.TryParse(range[1], out lTemp))
{
return false; // 起始位置无法解析
}
lFrom = lTemp;
}
// 3. 解析结束位置
if (range[2].Length <= 0)
{
lTo = -1; // 没有指定结束位置 Range: bytes=1234- 的情况
}
else
{
if (!long.TryParse(range[2], out lTemp)) // 排除 byte=xxxx- 的情况 TryParse 失败, 会把lTemp 置零
{
return false; // 第三度的内容不为空,但是无法解析
}
lTo = lTemp;
}
return true;
}
}